<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>

    <style>
        ul {
            display: flex;
            flex-wrap: wrap;
            gap: 10px;
            width: 300px;
            box-sizing: border-box;
        }

        li {
            list-style-type: none;
            background-color: lightcoral;
            width: 100px;
            padding: 5px;
            margin-top: 10px;
            border-radius: 3px;
            color: #fff;
            text-align: center;
            box-sizing: border-box;
            cursor: pointer;
            flex-shrink: 0;
        }

        .dragging {
            opacity: .5;
            border: 5px dashed #999;
        }
    </style>
</head>

<body>
    <ul>
        <li data-i="1" draggable="true">1</li>
        <li data-i="2" draggable="true">2</li>
        <li data-i="3" draggable="true">3</li>
        <li data-i="4" draggable="true">4</li>
        <li data-i="5" draggable="true">5</li>
        <li data-i="6" draggable="true">6</li>
    </ul>
    <script type="text/javascript">
        const DURATION = '.6s'

        init()


        function init() {
            bindDragEvent()
            useAnimate('ul')
        }

        function useAnimate(selector) {
            const ob = new MutationObserver((records) => {
                let befEl, aftEl

                befEl = records[0].removedNodes[0]
                aftEl = records[2].removedNodes[0]

                exchangePos(befEl, aftEl)
            })

            ob.observe(document.querySelector(selector), {
                childList: true
            })
        }

        function exchangePos(befEl, aftEl) {
            // `insertBefore`改变位置前的前后元素 也就是和上面相反
            const rawBef = aftEl,
                rawAft = befEl

            // 获取`Y`坐标
            const { left: befX, top: befY } = befEl.getBoundingClientRect(),
                { left: aftX, top: aftY } = aftEl.getBoundingClientRect(),
                { left: rawBefX, top: rawBefY } = rawBef.getBoundingClientRect(),
                { left: rawAftX, top: rawAftY } = rawAft.getBoundingClientRect()

            // 计算前后坐标差值 = 原始值 - 变化后的值
            const finalBefPos = [],
                finalAftPos = []
            finalBefPos.push(rawBefX - befX, rawBefY - befY)
            finalAftPos.push(rawAftX - aftX, rawAftY - aftY)

            applyAnimation(befEl, finalBefPos, aftEl, finalAftPos)
        }

        function applyAnimation(befEl, [befX, befY], aftEl, [aftX, aftY]) {
            // 用`transform` 先让他们位置复原
            befEl.style.transform = `translate(${befX}px, ${befY}px)`
            aftEl.style.transform = `translate(${aftX}px, ${aftY}px)`

            requestAnimationFrame(() => {
                befEl.style.transition = `transform ${DURATION}`
                aftEl.style.transition = `transform ${DURATION}`

                // 在下一次动画前 去除`transform` 也就是把位置变成`insertBefore`后 即移动后的位置
                befEl.style.transform = 'none'
                aftEl.style.transform = 'none'

                addEvent(befEl, aftEl)
            })
        }

        function addEvent(...els) {
            // 动画结束后 去除过渡
            els.forEach((el) => el.addEventListener('transitionend', () => {
                el.style.transition = 'none'
            }, { once: true }))
        }

        function bindDragEvent(bindEl = window) {
            let stEl, endEl

            bindEl.addEventListener('dragstart', ({ target }) => {
                if (target?.draggable) {
                    stEl = target
                    stEl.classList.add('dragging')
                }
                else {
                    stEl = null
                }
            })

            bindEl.addEventListener('dragenter', ({ target }) => {
                if (target?.draggable) {
                    target.classList.add('dragging')
                }
            })

            bindEl.addEventListener('dragover', e => e.preventDefault())

            bindEl.addEventListener('drop', ({ target }) => {
                if (!target?.draggable) {
                    return
                }
                endEl = target

                if (!stEl || !endEl) {
                    return
                }

                exchange(stEl, endEl)

                stEl.classList.remove('dragging')
                endEl.classList.remove('dragging')
                stEl = null
                endEl = null
            })

            bindEl.addEventListener('dragleave', ({ target }) => {
                target.draggable && target.classList.remove('dragging')
            })
        }

        function exchange(stEl, endEl) {
            const parent = endEl.parentNode,
                stElBrother = stEl.nextElementSibling,
                endElBrother = endEl.nextElementSibling

            const stI = stEl.dataset.i,
                endI = endEl.dataset.i

            parent.insertBefore(stEl, endElBrother)
            parent.insertBefore(endEl, stElBrother)
        }

    </script>
</body>

</html>